Hypothesis 1: Discordance is a relatively common, yet under appreciated phenomenon, and varies by geographic location of care and patient attributes including demographics, severity of illness, and underlying disease process.
Hypothesis 2: Discordance between aPTT and anti-Xa indicates the presence of coagulation system pathophysiology which is a risk factor for development of hemorrhage and thrombosis.
Classic systemic anticoagulation goals, anti-Xa vs aPTT, at NYP:
Typical anti-Xa goal vs aPTT goals among heparinized ECMO patients:
0.1 - 0.3 vs 35 - 50 (LOW GOAL)
0.2 - 0.5 vs 45 - 65 (CLASSIC GOAL)
Source: NYP GUIDLINE “UNFRACTIONATED HEPARIN INTRAVENOUS (IV) DOSING AND MONITORING FOR ADULT INPATIENTS RECEIVING EXTRACORPOREAL MEMBRANE OXYGENATION (ECMO)” Approved Dec 2022
Only first ECMO run used.
ECMO duration: Dr. Connie Nguyen manually reviewed heparin-exposed patients’ EMRs in June 2025 to identify true ECMO time off, as initial data review identified some labs that were drawn while on heparin but after the patient was off ECMO. The variable “time_off” in the original data was obtained from ELSO sources. Per Dr. Nguyen’s review, if her evaluation of ECMO stop time was within 4 hours of ELSO-derived time_off, then the actual true time off was deemed as that known by ELSO. If there was a greater than 4 hour discrepancy, a new time_off_true value was recorded.
Lab cutoffs:
if aPTT <20.0 - made 0
if aPTT >180 - made 180
if anti-Xa <0.04 or <0.1 - made 0
if anti-Xa >2.00 - made 2.0
if data is duplicated at a specific timepoint (e.g. 2 lab samples, exact same result time but different values), then taking the mean of the two exact same-timed values
Hyperbilirubinemia can artifactually elevate anti-Xa (colorimetric assay). Jaundice threshold is 2 - 3 mg/dL total bilirubin. Upper limit of normal total bilirubin at NYP-CUIMC is 1.2 mg/dL.
Bilirubin is measured in separate tubes compared to aPTT, anti-Xa,
INR, and fibrinogen. As hyperbilirubinemia can impact assessment of
anti-Xa but also may not change rapidly from one moment to another, we
accounted for total bilirubin by computing the
nearest_bilirubin value within 24 hours of any aPTT. (aPTT
chosen as it was the most frequently measured coagulation lab).
Likewise, platelet count is measured in separate tubes compared to
aPTT, anti-Xa, INR, and fibrinogen. Thrombocytopenia can impact
discordance (PMID 38725133). Thus, we computed
nearest_platelet value within 24 hours of any aPTT, in a
manner comparable to that of nearest_bilirubin.
COMPLICATIONS: all complications reviewed were documented during ECMO duration, with exception of clinically significant bleeding and thrombotic complications which were manually assessed by Connie and Phil as follows:
DVT/PE_1_time_and_date
DVT/PE_2_time_and_date
DVT/PE_3_time_and_date
DVT/PE_4_time_and_date
Ischemic_stroke_1_time_and_date
Ischemic_stroke_2_time_and_date
Ischemic_stroke_3_time_and_date
Acute_limb_ischemia_1_time_and_date
Acute_limb_ischemia_2_time_and_date
Acute_limb_ischemia_3_time_and_date
GI bleeding - present on CTA and confirmed via endoscopy
ICH - based on imaging findings
| n |
|---|
| 165 |
| department_name | n |
|---|---|
| MIL 5 CTICU | 76 |
| MIL 5 CCU | 41 |
| MIL 4 MICU A | 31 |
| HRT CARDIAC CARE | 25 |
| MIL OPERATING ROOM | 5 |
| MIL 4 SICU | 1 |
| MIL CARDIAC CATH | 1 |
| GBG 4 W CT ICU | 1 |
| n |
|---|
| 113 |
| department_name | n |
|---|---|
| MIL 5 CTICU | 56 |
| MIL 5 CCU | 31 |
| HRT CARDIAC CARE | 22 |
| MIL 4 MICU A | 6 |
| MIL 4 SICU | 1 |
| MIL OPERATING ROOM | 1 |
| MIL CARDIAC CATH | 1 |
| GBG 4 W CT ICU | 1 |
## # A tibble: 1 × 1
## correlation
## <dbl>
## 1 0.740
This fits a second degree polynomial on aPTT and accounts for repeated measures per patient (i.e. multiple labs per individual patient) by including a random intercept for each patient. This is important as the repeated measures per patient are not independent - the labs are dependent on the baseline values (and the baseline anti-Xa between patients who have the same aPTT may not be the same, or vice versa).
95% prediction interval chosen to balance sensitivity and specificity.
|
|
## Adding missing grouping variables: `mrn`, `lab_result_time`
| mrn | lab_result_time | activated_partial_thromboplastin_time | heparin_assay_quantitative |
|---|---|---|---|
| 1001491594 | 2024-08-24 22:45:00 | 96.9 | 0.36 |
| 1002467250 | 2024-12-12 18:30:00 | 63.5 | 0.26 |
| 1006905710 | 2024-10-10 18:02:00 | 71.3 | 0.07 |
| 1006905710 | 2024-10-11 06:23:00 | 56.2 | 0.16 |
| 1007129753 | 2024-04-12 06:13:00 | 43.5 | 0.10 |
| 1007129753 | 2024-04-12 18:41:00 | 42.9 | 0.10 |
| 1007129753 | 2024-04-13 06:08:00 | 43.8 | 0.10 |
| 1007129753 | 2024-04-13 18:00:00 | 45.2 | 0.10 |
| 1009477257 | 2024-12-02 15:04:00 | 41.9 | 0.09 |
| 1010147999 | 2024-02-20 18:35:00 | 60.3 | 0.10 |
| 1010147999 | 2024-02-21 12:35:00 | 52.6 | 0.10 |
| 1011043983 | 2024-06-17 17:05:00 | 75.4 | 0.33 |
| 1011043983 | 2024-06-17 22:39:00 | 93.2 | 0.37 |
| 1011043983 | 2024-06-18 04:34:00 | 93.8 | 0.36 |
| 1011043983 | 2024-06-18 09:40:00 | 106.5 | 0.40 |
| 1011043983 | 2024-06-18 16:00:00 | 103.4 | 0.43 |
| 1011043983 | 2024-06-18 22:46:00 | 89.5 | 0.37 |
| 1011043983 | 2024-06-19 05:51:00 | 106.0 | 0.43 |
| 1011043983 | 2024-06-19 11:46:00 | 122.7 | 0.46 |
| 1011043983 | 2024-06-20 11:59:00 | 104.6 | 0.48 |
| 1011077110 | 2024-05-20 06:06:00 | 58.1 | 0.14 |
| 1011077110 | 2024-05-21 06:42:00 | 70.1 | 0.23 |
| 1100547209 | 2024-05-02 14:06:00 | 119.4 | 0.60 |
| 1102078503 | 2025-01-07 10:13:00 | 36.9 | 0.04 |
| 1201199342 | 2024-09-26 12:28:00 | 76.7 | 0.34 |
| 1400032239 | 2024-03-18 23:46:00 | 45.6 | 0.10 |
| 1400969240 | 2024-10-13 23:20:00 | 39.7 | 0.05 |
| 1400969240 | 2024-10-14 18:13:00 | 38.5 | 0.06 |
| 1401183967 | 2024-03-30 18:34:00 | 102.0 | 0.30 |
| 1401327774 | 2024-11-03 03:56:00 | 122.1 | 0.38 |
| 1401327774 | 2024-11-03 11:13:00 | 94.3 | 0.38 |
| 1401327774 | 2024-11-03 19:02:00 | 63.2 | 0.22 |
| 1401468984 | 2024-02-24 07:04:00 | 42.8 | 0.10 |
| 1401468984 | 2024-02-27 18:47:00 | 55.3 | 0.20 |
| 1401639110 | 2024-08-20 06:59:00 | 64.4 | 0.16 |
| 1401766288 | 2024-07-31 06:09:00 | 60.3 | 0.21 |
## # A tibble: 1 × 2
## mean_aptt mean_antiXa
## <dbl> <dbl>
## 1 72.3 0.241
| concordance_classic_simple | n |
|---|---|
| concordant | 13 |
| discordant aPTT above antiXa | 22 |
| high aPTT low antiXa | 1 |
## Adding missing grouping variables: `mrn`, `lab_result_time`
| concordance_low_simple | n |
|---|---|
| concordant | 20 |
| discordant aPTT above antiXa | 15 |
| high aPTT low antiXa | 1 |
| mrn | lab_result_time | activated_partial_thromboplastin_time | heparin_assay_quantitative |
|---|---|---|---|
| 1001491594 | 2024-08-20 04:09:00 | 41.8 | 0.47 |
| 1001491594 | 2024-08-20 10:16:00 | 55.3 | 0.59 |
| 1001491594 | 2024-08-20 21:41:00 | 61.2 | 0.67 |
| 1001491594 | 2024-08-21 05:17:00 | 66.8 | 0.74 |
| 1001491594 | 2024-08-22 09:33:00 | 46.4 | 0.43 |
| 1003642179 | 2024-01-12 05:15:00 | 63.9 | 0.56 |
| 1009544965 | 2024-01-31 14:58:00 | 98.6 | 0.90 |
| 1009544965 | 2024-02-01 01:05:00 | 92.9 | 0.80 |
| 1009544965 | 2024-02-01 17:11:00 | 68.1 | 0.60 |
| 1009544965 | 2024-02-02 00:55:00 | 57.2 | 0.50 |
| 1010696012 | 2024-04-01 03:44:00 | 44.3 | 0.40 |
| 1010696012 | 2024-04-01 09:37:00 | 46.2 | 0.40 |
| 1010696012 | 2024-04-03 06:25:00 | 35.1 | 0.30 |
| 1101397979 | 2024-02-23 22:03:00 | 170.7 | 1.70 |
| 1101397979 | 2024-02-28 12:44:00 | 55.8 | 0.50 |
| 1102216636 | 2024-08-29 04:04:00 | 67.8 | 0.62 |
| 1201555171 | 2024-10-05 01:13:00 | 44.3 | 0.40 |
| 1201555171 | 2024-10-05 12:40:00 | 56.2 | 0.63 |
| 1201555171 | 2024-10-05 19:00:00 | 53.4 | 0.57 |
| 1201555171 | 2024-10-06 01:07:00 | 44.7 | 0.45 |
| 1201555171 | 2024-10-06 06:29:00 | 38.2 | 0.34 |
| 1401554006 | 2024-01-15 03:42:00 | 39.2 | 0.33 |
| 1401554006 | 2024-01-15 18:16:00 | 41.4 | 0.39 |
| 1401554006 | 2024-01-16 05:37:00 | 43.8 | 0.39 |
| 1401591974 | 2024-02-08 03:45:00 | 70.3 | 0.70 |
| 1401591974 | 2024-02-10 17:55:00 | 44.5 | 0.40 |
| 1401591974 | 2024-02-12 05:09:00 | 53.4 | 0.50 |
| 1401611898 | 2024-04-10 12:45:00 | 48.0 | 0.40 |
| 1401611898 | 2024-04-11 07:22:00 | 35.4 | 0.30 |
| 1401611898 | 2024-04-11 20:24:00 | 46.9 | 0.40 |
| 1401611898 | 2024-04-12 05:14:00 | 41.9 | 0.40 |
| 1401611898 | 2024-04-12 12:44:00 | 43.7 | 0.40 |
| 1401682296 | 2024-04-17 04:01:00 | 36.3 | 0.30 |
| 1401683148 | 2024-05-24 05:06:00 | 51.2 | 0.40 |
| 1401689750 | 2024-04-22 05:22:00 | 60.2 | 0.60 |
| 1401689750 | 2024-04-23 05:38:00 | 45.7 | 0.50 |
| 1401689750 | 2024-04-24 04:17:00 | 39.8 | 0.40 |
| 1401689750 | 2024-04-25 04:24:00 | 39.4 | 0.40 |
| 1401696145 | 2024-05-07 19:31:00 | 47.0 | 0.40 |
| 1401697961 | 2024-04-29 19:21:00 | 33.8 | 0.30 |
| 1401707171 | 2024-05-10 19:23:00 | 39.4 | 0.50 |
| 1401707171 | 2024-05-11 04:21:00 | 34.0 | 0.30 |
| 1401707171 | 2024-05-11 11:42:00 | 30.7 | 0.30 |
| 1401707171 | 2024-05-12 07:19:00 | 34.4 | 0.40 |
| 1401707171 | 2024-05-12 12:43:00 | 34.9 | 0.30 |
| 1401707171 | 2024-05-12 22:13:00 | 35.1 | 0.40 |
| 1401707171 | 2024-05-13 03:56:00 | 35.5 | 0.40 |
| 1401707171 | 2024-05-13 21:40:00 | 28.6 | 0.30 |
| 1401796928 | 2024-07-25 18:01:00 | 43.9 | 0.51 |
| 1401848048 | 2024-09-10 01:40:00 | 48.4 | 0.43 |
| 1401848048 | 2024-09-10 06:43:00 | 53.5 | 0.50 |
| 1401848048 | 2024-09-10 12:27:00 | 53.2 | 0.54 |
| 1401991907 | 2024-12-09 06:48:00 | 39.2 | 0.34 |
| 1401991907 | 2024-12-09 19:11:00 | 40.2 | 0.36 |
| 1401991907 | 2024-12-10 00:53:00 | 40.0 | 0.37 |
| 1401991907 | 2024-12-10 06:26:00 | 36.4 | 0.35 |
| 1401991907 | 2024-12-10 13:57:00 | 39.8 | 0.34 |
| 1401991907 | 2024-12-10 18:34:00 | 38.6 | 0.38 |
## # A tibble: 1 × 2
## mean_aptt mean_antiXa
## <dbl> <dbl>
## 1 49.6 0.474
## Adding missing grouping variables: `mrn`, `lab_result_time`
| concordance_classic_simple | n |
|---|---|
| concordant | 19 |
| discordant aPTT below antiXa | 38 |
| low aPTT high antiXa | 1 |
| concordance_low_simple | n |
|---|---|
| concordant | 22 |
| discordant aPTT below antiXa | 35 |
| low aPTT high antiXa | 1 |
| mrn | n |
|---|---|
| 1001491594 | 2 |
## # A tibble: 7 × 3
## # Groups: lab_result_time, unusual_type [7]
## lab_result_time unusual_type n
## <dttm> <fct> <int>
## 1 2024-08-20 04:09:00 aPTT < antiXa 1
## 2 2024-08-20 10:16:00 aPTT < antiXa 1
## 3 2024-08-20 21:41:00 aPTT < antiXa 1
## 4 2024-08-21 05:17:00 aPTT < antiXa 1
## 5 2024-08-22 09:33:00 aPTT < antiXa 1
## 6 2024-08-23 05:28:00 concordant 1
## 7 2024-08-24 22:45:00 aPTT > antiXa 1
Study ID 5 (MRN 1001491594) had both discordance types (aPTT > antiXa and aPTT < antiXa).
Perhaps it would be worth retaining this patient in the model, as discordance could contribute to hemorrhage and/or thrombosis in separate instances.
| unusual_type | n |
|---|---|
| concordant | 477 |
| aPTT > antiXa | 36 |
| aPTT < antiXa | 58 |
## # A tibble: 6 × 4
## conc high low n
## <lgl> <lgl> <lgl> <int>
## 1 FALSE FALSE TRUE 5
## 2 FALSE TRUE FALSE 3
## 3 TRUE FALSE FALSE 77
## 4 TRUE FALSE TRUE 13
## 5 TRUE TRUE FALSE 14
## 6 TRUE TRUE TRUE 1
## # A tibble: 3 × 2
## first_discord_type n
## <chr> <int>
## 1 aPTT < antiXa 19
## 2 aPTT > antiXa 17
## 3 concordant 77
Interpreting:
77 patients: always concordant
17 patients: ever high only (aPTT > antiXa)
19 patients: ever low only (aPTT < antiXa)
1 patient: both high AND low
Note that one patient with both discordance groups is included in this analysis. This patient’s first discordance type was “aPTT < antiXa”
| discord_group | n_patients | median_age | iqr_age | male_pct | median_wt | iqr_wt | median_ht | iqr_ht | median_hours | iqr_hours | cticu_pct | ccu_pct | micu_pct | otherloc_pct | vv_pct | va_pct | female_pct | median_aptt | iqr_appt | median_antixa | iqr_antixa | median_inr | iqr_inr | median_fibrinogen | iqr_fibrinogen | median_bilirubin | iqr_bilirubin | median_platelets | iqr_platelets | median_hep_dose | iqr_hep_dose |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| aPTT < antiXa | 18 | 60 | 19 | 55.56 | 99.05 | 22.5 | 171.6 | 13.30 | 164.5 | 114.75 | 16.67 | 83.33 | 0.00 | 0.00 | 5.56 | 83.33 | 44.44 | 42.90 | 11.44 | 0.36 | 0.14 | 1.2 | 0.14 | 349.0 | 274.62 | 0.7 | 1.55 | 123.75 | 58.5 | 9.5 | 6 |
| aPTT > antiXa | 17 | 58 | 16 | 52.94 | 73.00 | 20.4 | 167.6 | 13.00 | 192.0 | 259.00 | 64.71 | 29.41 | 0.00 | 5.88 | 5.88 | 76.47 | 47.06 | 50.55 | 12.90 | 0.17 | 0.13 | 1.2 | 0.17 | 466.5 | 267.00 | 2.0 | 1.10 | 85.50 | 65.0 | 9.0 | 6 |
| never discordant | 77 | 56 | 21 | 67.53 | 84.80 | 29.7 | 170.2 | 12.93 | 114.0 | 108.00 | 53.25 | 36.36 | 7.79 | 2.60 | 16.88 | 63.64 | 32.47 | 34.00 | 10.15 | 0.15 | 0.12 | 1.1 | 0.20 | 477.5 | 322.00 | 0.8 | 0.95 | 103.00 | 61.5 | 6.0 | 6 |
| variable | p_value |
|---|---|
| age_years | 0.8123 |
| weight | 0.0013 |
| height | 0.3491 |
| hours_ecmo | 0.0420 |
| activated_partial_thromboplastin_time | 0.0000 |
| heparin_assay_quantitative | 0.0000 |
| international_normalization_ratio | 0.0823 |
| fibrinogen | 0.2517 |
| nearest_bilirubin | 0.0121 |
| nearest_platelet | 0.0453 |
| heparin_infusion_dose_value_units_kg_hr | 0.0192 |
| variable | group1 | group2 | p.value |
|---|---|---|---|
| age_years | aPTT > antiXa | aPTT < antiXa | 0.8171 |
| age_years | never discordant | aPTT < antiXa | 0.7359 |
| age_years | never discordant | aPTT > antiXa | 0.5554 |
| weight | aPTT > antiXa | aPTT < antiXa | 0.0003 |
| weight | never discordant | aPTT < antiXa | 0.0169 |
| weight | never discordant | aPTT > antiXa | 0.0248 |
| height | aPTT > antiXa | aPTT < antiXa | 0.2033 |
| height | never discordant | aPTT < antiXa | 0.6203 |
| height | never discordant | aPTT > antiXa | 0.2046 |
| hours_ecmo | aPTT > antiXa | aPTT < antiXa | 0.5747 |
| hours_ecmo | never discordant | aPTT < antiXa | 0.1699 |
| hours_ecmo | never discordant | aPTT > antiXa | 0.0191 |
| activated_partial_thromboplastin_time | aPTT > antiXa | aPTT < antiXa | 0.1949 |
| activated_partial_thromboplastin_time | never discordant | aPTT < antiXa | 0.0008 |
| activated_partial_thromboplastin_time | never discordant | aPTT > antiXa | 0.0000 |
| heparin_assay_quantitative | aPTT > antiXa | aPTT < antiXa | 0.0000 |
| heparin_assay_quantitative | never discordant | aPTT < antiXa | 0.0000 |
| heparin_assay_quantitative | never discordant | aPTT > antiXa | 0.3838 |
| international_normalization_ratio | aPTT > antiXa | aPTT < antiXa | 0.5597 |
| international_normalization_ratio | never discordant | aPTT < antiXa | 0.1807 |
| international_normalization_ratio | never discordant | aPTT > antiXa | 0.0472 |
| fibrinogen | aPTT > antiXa | aPTT < antiXa | 0.2544 |
| fibrinogen | never discordant | aPTT < antiXa | 0.1218 |
| fibrinogen | never discordant | aPTT > antiXa | 0.8572 |
| nearest_bilirubin | aPTT > antiXa | aPTT < antiXa | 0.0643 |
| nearest_bilirubin | never discordant | aPTT < antiXa | 0.6826 |
| nearest_bilirubin | never discordant | aPTT > antiXa | 0.0027 |
| nearest_platelet | aPTT > antiXa | aPTT < antiXa | 0.0167 |
| nearest_platelet | never discordant | aPTT < antiXa | 0.2151 |
| nearest_platelet | never discordant | aPTT > antiXa | 0.0589 |
| heparin_infusion_dose_value_units_kg_hr | aPTT > antiXa | aPTT < antiXa | 0.3721 |
| heparin_infusion_dose_value_units_kg_hr | never discordant | aPTT < antiXa | 0.0071 |
| heparin_infusion_dose_value_units_kg_hr | never discordant | aPTT > antiXa | 0.1874 |
| variable | aPTT < antiXa | aPTT > antiXa | never discordant | p_value |
|---|---|---|---|---|
| activated_partial_thromboplastin_time | 42.9 (IQR 11.4) | 50.5 (IQR 12.9) | 34.0 (IQR 10.2) | 0.0000 |
| age_years | 60.0 (IQR 19.0) | 58.0 (IQR 16.0) | 56.0 (IQR 21.0) | 0.8123 |
| fibrinogen | 349.0 (IQR 274.6) | 466.5 (IQR 267.0) | 477.5 (IQR 322.0) | 0.2517 |
| height | 171.6 (IQR 13.3) | 167.6 (IQR 13.0) | 170.2 (IQR 12.9) | 0.3491 |
| heparin_assay_quantitative | 0.4 (IQR 0.1) | 0.2 (IQR 0.1) | 0.2 (IQR 0.1) | 0.0000 |
| heparin_infusion_dose_value_units_kg_hr | 9.5 (IQR 6.0) | 9.0 (IQR 6.0) | 6.0 (IQR 6.0) | 0.0192 |
| hours_ecmo | 164.5 (IQR 114.8) | 192.0 (IQR 259.0) | 114.0 (IQR 108.0) | 0.0420 |
| international_normalization_ratio | 1.2 (IQR 0.1) | 1.2 (IQR 0.2) | 1.1 (IQR 0.2) | 0.0823 |
| nearest_bilirubin | 0.7 (IQR 1.6) | 2.0 (IQR 1.1) | 0.8 (IQR 0.9) | 0.0121 |
| nearest_platelet | 123.8 (IQR 58.5) | 85.5 (IQR 65.0) | 103.0 (IQR 61.5) | 0.0453 |
| weight | 99.0 (IQR 22.5) | 73.0 (IQR 20.4) | 84.8 (IQR 29.7) | 0.0013 |
| variable | p_value |
|---|---|
| sex | 0.3975 |
| department_name | 0.0263 |
| mode | 0.7435 |
| support_type | 0.0118 |
| covid19 | 0.1969 |
| variable | aPTT < antiXa | aPTT > antiXa | never discordant |
|---|---|---|---|
| Female | 8 (44.4%) | 8 (47.1%) | 25 (32.5%) |
| Male | 10 (55.6%) | 9 (52.9%) | 52 (67.5%) |
| MIL 5 CCU | 10 (55.6%) | 2 (11.8%) | 16 (20.8%) |
| MIL 5 CTICU | 3 (16.7%) | 11 (64.7%) | 40 (51.9%) |
| MIL 4 SICU | NA | NA | 1 (1.3%) |
| MIL OPERATING ROOM | NA | NA | 1 (1.3%) |
| HRT CARDIAC CARE | 5 (27.8%) | 3 (17.6%) | 12 (15.6%) |
| MIL 4 MICU A | NA | NA | 6 (7.8%) |
| MIL CARDIAC CATH | NA | 1 (5.9%) | NA |
| GBG 4 W CT ICU | NA | NA | 1 (1.3%) |
| Conversion | 2 (11.1%) | 3 (17.6%) | 13 (16.9%) |
| VA | 15 (83.3%) | 13 (76.5%) | 49 (63.6%) |
| VV | 1 (5.6%) | 1 (5.9%) | 13 (16.9%) |
| VVA | NA | NA | 2 (2.6%) |
| Cardiac | 17 (94.4%) | 14 (82.4%) | 48 (62.3%) |
| ECPR | NA | 2 (11.8%) | 4 (5.2%) |
| Pulmonary | 1 (5.6%) | 1 (5.9%) | 25 (32.5%) |
| 4 | 7 (38.9%) | 5 (29.4%) | 12 (15.6%) |
| NA | 11 (61.1%) | 12 (70.6%) | 65 (84.4%) |
Note that the one patient with both discordance groups is included in this analysis had a FIRST discordance type was “aPTT < antiXa”. This patient is dropped for between-group comparison.
| name | concordant | aPTT > antiXa | aPTT < antiXa |
|---|---|---|---|
| n_patients | 96.00 | 7.00 | 10.00 |
| n_labs | 473.00 | 45.00 | 53.00 |
| median_age | 56.00 | 58.00 | 51.00 |
| iqr_age | 23.00 | 7.00 | 28.00 |
| male_pct | 63.42 | 91.11 | 56.60 |
| median_wt | 86.00 | 83.90 | 98.50 |
| iqr_wt | 30.70 | 13.00 | 9.10 |
| median_ht | 170.20 | 167.60 | 175.00 |
| iqr_ht | 15.20 | 7.70 | 17.80 |
| median_hours | 203.00 | 106.00 | 189.00 |
| iqr_hours | 241.00 | 248.00 | 100.00 |
| cticu_pct | 39.96 | 28.89 | 24.53 |
| ccu_pct | 38.05 | 2.22 | 39.62 |
| hrtccu_pct | 18.82 | 66.67 | 35.85 |
| micu_pct | 2.11 | 0.00 | 0.00 |
| vv_pct | 10.15 | 0.00 | 1.89 |
| va_pct | 67.65 | 100.00 | 96.23 |
| vva_pct | 2.54 | 0.00 | 0.00 |
| convert_pct | 19.66 | 0.00 | 1.89 |
| median_flow4 | 3.10 | 3.20 | 2.80 |
| iqr_flow4 | 0.80 | 0.20 | 1.00 |
| median_flow24 | 3.30 | 3.00 | 3.40 |
| iqr_flow24 | 0.70 | 0.20 | 0.60 |
| median_aptt | 37.40 | 58.10 | 41.80 |
| iqr_aptt | 14.20 | 43.10 | 21.30 |
| median_antixa | 0.18 | 0.30 | 0.38 |
| iqr_antixa | 0.19 | 0.18 | 0.26 |
| median_inr | 1.10 | 1.20 | 1.20 |
| iqr_inr | 0.20 | 0.10 | 0.20 |
| median_fibrinogen | 482.00 | 592.50 | 300.50 |
| iqr_fibrinogen | 349.00 | 246.50 | 153.75 |
| median_heparin_dose | 9.00 | 12.00 | 8.00 |
| iqr_heparin_dose | 6.50 | 6.00 | 4.00 |
| median_nearest_bilirubin | 1.10 | 1.70 | 0.70 |
| iqr_nearest_bilirubin | 1.20 | 1.60 | 0.40 |
##
## concordant aPTT > antiXa aPTT < antiXa
## ccu 271 16 53
## cticu 191 19 5
## micu 10 0 0
##
## Fisher's Exact Test for Count Data
##
## data: table_locations
## p-value = 1.245e-06
## alternative hypothesis: two.sided
##
## concordant aPTT > antiXa aPTT < antiXa
## Cardiac 365 31 57
## ECPR 32 3 0
## Pulmonary 80 2 1
## mode
## unusual_type VA VV
## concordant 338 46
## aPTT > antiXa 30 2
## aPTT < antiXa 48 1
##
## Fisher's Exact Test for Count Data
##
## data: .
## p-value = 0.05746
## alternative hypothesis: two.sided
Safety checks to ensure only including labs and complication that were measured between ECMO start and 7 days following ECMO STOP
# safety check to ensure only including labs that were measured after ecmo start or before 7 days following ECMO stop
model_fit_data %>%
filter(lab_result_time < time_on | lab_result_time > complication_7day_assessment)
## # A tibble: 0 × 143
## # ℹ 143 variables: study_id <int>, mrn <chr>, patient_name <chr>,
## # time_on <dttm>, time_off <dttm>, time_off_true <dttm>,
## # complication_7day_assessment <dttm>, duration_to_lab <dbl>,
## # patient_id <chr>, run_id <chr>, lab_result_time <dttm>,
## # department_name <fct>, heparin_infusion_dose_value_units_kg_hr <dbl>,
## # concordance_classic <fct>, concordance_low <fct>,
## # concordance_classic_simple <fct>, concordance_low_simple <fct>, …
## check shows no data -> all is okay
#safety check to ensure only including complications that were measured after ecmo start or before 7 days following ECMO stop
model_fit_data %>%
pivot_longer(
death_date:ich_based_on_imaging_findings,
names_to = "complication",
values_to = "time_complication") %>%
filter(time_complication < time_on | time_complication > complication_7day_assessment)
## # A tibble: 0 × 102
## # ℹ 102 variables: study_id <int>, mrn <chr>, patient_name <chr>,
## # time_on <dttm>, time_off <dttm>, time_off_true <dttm>,
## # complication_7day_assessment <dttm>, duration_to_lab <dbl>,
## # patient_id <chr>, run_id <chr>, lab_result_time <dttm>,
## # department_name <fct>, heparin_infusion_dose_value_units_kg_hr <dbl>,
## # concordance_classic <fct>, concordance_low <fct>,
## # concordance_classic_simple <fct>, concordance_low_simple <fct>, …
## check shows no data -> all is okay
Complications are assessed only if they occur after a lab (discordance) result time and before 7 days after ECMO stop.
Created composite hemorrhagic and thrombotic outcomes, grouped as below.
Any Hemorrhagic Complication =
hemorrhage_any
neurologic_intra_extra_parenchymal_cns_hemorrhage_us_or_ct_or_mri
hemorrhagic_gi_hemorrhage
major_bleeding_perfusion
hemorrhagic_peripheral_cannulation_site_bleeding
pulmonary_pulmonary_hemorrhage
hemorrhagic_surgical_site_bleeding
hemorrhagic_mediastinal_cannulation_site_bleeding
gi_bleeding_present_on_cta_and_confirmed_via_endoscopy***
ich_based_on_imaging_findings***
Any Thrombotic Complication =
thrombosis_any
neurologic_cns_infarction_us_or_ct_or_mri
limb_ischemia
mechanical_thrombosis_clots_circuit_component
circuit_thrombosis_perfusion
dvt_pe_1_time_and_date***
ischemic_stroke_1_time_and_date***
acute_limb_ischemia_1_time_and_date***
not included in thrombosis composite as unlikely to be biologically related to discordance:
neurologic_cns_diffuse_ischemia_ct_mri
limb_fasciotomy
*** = complications as reviewed manually by Phil/Connie
Skipped as could violate causal time pathway between discordance -> complication
dvt_pe_2_time_and_date
dvt_pe_3_time_and_date
dvt_pe_4_time_and_date
acute_limb_ischemia_2_time_and_date
acute_limb_ischemia_3_time_and_date
ischemic_stroke_2_time_and_date
ischemic_stroke_3_time_and_date
| complication | aPTT < antiXa | aPTT > antiXa | never discordant |
|---|---|---|---|
| any_acute_limb_ischemia_1_time_and_date | 0 | 2 | 7 |
| any_circuit_thrombosis_perfusion | 3 | 1 | 15 |
| any_dvt_pe_1_time_and_date | 1 | 2 | 14 |
| any_gi_bleeding_present_on_cta_and_confirmed_via_endoscopy | 0 | 1 | 0 |
| any_hemorrhagic_gi_hemorrhage | 0 | 0 | 1 |
| any_hemorrhagic_mediastinal_cannulation_site_bleeding | 0 | 0 | 0 |
| any_hemorrhagic_peripheral_cannulation_site_bleeding | 0 | 1 | 0 |
| any_hemorrhagic_surgical_site_bleeding | 0 | 0 | 1 |
| any_ich_based_on_imaging_findings | 1 | 2 | 0 |
| any_ischemic_stroke_1_time_and_date | 0 | 2 | 2 |
| any_limb_ischemia | 0 | 0 | 0 |
| any_major_bleeding_perfusion | 0 | 1 | 1 |
| any_mechanical_thrombosis_clots_circuit_component | 0 | 0 | 0 |
| any_neurologic_cns_infarction_us_or_ct_or_mri | 0 | 1 | 0 |
| any_neurologic_intra_extra_parenchymal_cns_hemorrhage_us_or_ct_or_mri | 0 | 0 | 0 |
| any_pulmonary_pulmonary_hemorrhage | 0 | 0 | 0 |
| hemorrhage_any | 1 | 4 | 3 |
| thrombosis_any | 4 | 6 | 34 |
patient_model_data <- patient_level_complications %>%
left_join(patient_demographics, by = "mrn") %>%
filter(discord_group != "both types") %>%
mutate(
discord_group = fct_relevel(discord_group, "never discordant"),
icu_type = case_when(
department_name == "MIL 5 CCU" ~ "ccu",
department_name == "MIL 5 CTICU" ~ "cticu",
department_name == "MIL 4 SICU" ~ "other",
department_name == "MIL OPERATING ROOM" ~ "other",
department_name == "HRT CARDIAC CARE" ~ "ccu",
department_name == "MIL 4 MICU A" ~ "micu",
department_name == "MIL CARDIAC CATH" ~ "other",
department_name == "PBY ADULT EMERGENCY" ~ "other",
department_name == "MIL 4 MICU B" ~ "micu",
department_name == "GBG 4 W CT ICU" ~ "other",
department_name == "MIL 9 HUDSON" ~ "other",
department_name == "POST-DISCHARGE" ~ "other",
TRUE ~ NA)) %>%
filter(icu_type != "other")
Fibrinogen only measured in 38 patients, thus cannot be included as a covariable
Note that this does not preserve time-matching of labs, which is problematic, and is based
hemorrhage_patient_glm <- glm(
hemorrhage_any ~ discord_group,
data = patient_model_data,
family = binomial)
hemorrhage_patient_glm %>% broom::tidy(exponentiate = TRUE, conf.int = TRUE) %>% kable(digits = 3, caption = "Univariable regression with composite hemorrhage outcome")
| term | estimate | std.error | statistic | p.value | conf.low | conf.high |
|---|---|---|---|---|---|---|
| (Intercept) | 0.042 | 0.589 | -5.368 | 0.000 | 0.010 | 0.113 |
| discord_groupaPTT < antiXa | 1.392 | 1.186 | 0.279 | 0.780 | 0.067 | 11.659 |
| discord_groupaPTT > antiXa | 7.889 | 0.825 | 2.503 | 0.012 | 1.560 | 44.499 |
hemorrhage_multivariable_patient_glm <-glm(
hemorrhage_any ~ discord_group + international_normalization_ratio + nearest_bilirubin + heparin_infusion_dose_value_units_kg_hr + weight + nearest_platelet,
data = patient_model_data,
family = binomial)
hemorrhage_multivariable_patient_glm %>% broom::tidy(exponentiate = TRUE, conf.int = TRUE) %>% kable(digits = 3, caption = "Multivariable regression with composite outcome")
| term | estimate | std.error | statistic | p.value | conf.low | conf.high |
|---|---|---|---|---|---|---|
| (Intercept) | 0.005 | 2.830 | -1.904 | 0.057 | 0.000 | 0.773 |
| discord_groupaPTT < antiXa | 1.356 | 1.455 | 0.209 | 0.834 | 0.043 | 19.661 |
| discord_groupaPTT > antiXa | 3.987 | 0.978 | 1.415 | 0.157 | 0.575 | 29.478 |
| international_normalization_ratio | 70.410 | 2.295 | 1.853 | 0.064 | 1.010 | 9931.450 |
| nearest_bilirubin | 0.780 | 0.423 | -0.588 | 0.556 | 0.288 | 1.557 |
| heparin_infusion_dose_value_units_kg_hr | 1.089 | 0.116 | 0.736 | 0.462 | 0.866 | 1.380 |
| weight | 0.971 | 0.025 | -1.196 | 0.232 | 0.919 | 1.014 |
| nearest_platelet | 0.994 | 0.012 | -0.476 | 0.634 | 0.969 | 1.017 |
thrombosis_patient_glm <- glm(
thrombosis_any ~ discord_group,
data = patient_model_data,
family = binomial)
thrombosis_patient_glm %>% broom::tidy(exponentiate = TRUE, conf.int = TRUE) %>% kable(digits = 3, caption = "Univariable regression with thrombosis composite outcome")
| term | estimate | std.error | statistic | p.value | conf.low | conf.high |
|---|---|---|---|---|---|---|
| (Intercept) | 0.805 | 0.234 | -0.928 | 0.353 | 0.506 | 1.271 |
| discord_groupaPTT < antiXa | 0.355 | 0.613 | -1.689 | 0.091 | 0.094 | 1.098 |
| discord_groupaPTT > antiXa | 0.745 | 0.567 | -0.518 | 0.604 | 0.233 | 2.223 |
thrombosis_multivariable_patient_glm <-glm(
thrombosis_any ~ discord_group + international_normalization_ratio + nearest_bilirubin + heparin_infusion_dose_value_units_kg_hr + nearest_platelet + weight,
data = patient_model_data,
family = binomial)
thrombosis_multivariable_patient_glm %>% broom::tidy(exponentiate = TRUE, conf.int = TRUE) %>% kable(digits = 3, caption = "Multivariable regression with thrombosis composite outcome")
| term | estimate | std.error | statistic | p.value | conf.low | conf.high |
|---|---|---|---|---|---|---|
| (Intercept) | 0.611 | 1.619 | -0.304 | 0.761 | 0.026 | 16.483 |
| discord_groupaPTT < antiXa | 0.531 | 0.676 | -0.937 | 0.349 | 0.126 | 1.894 |
| discord_groupaPTT > antiXa | 0.723 | 0.652 | -0.497 | 0.619 | 0.188 | 2.539 |
| international_normalization_ratio | 2.541 | 1.328 | 0.702 | 0.483 | 0.177 | 35.738 |
| nearest_bilirubin | 0.841 | 0.187 | -0.928 | 0.353 | 0.560 | 1.190 |
| heparin_infusion_dose_value_units_kg_hr | 0.954 | 0.060 | -0.785 | 0.433 | 0.845 | 1.072 |
| nearest_platelet | 1.001 | 0.005 | 0.120 | 0.905 | 0.990 | 1.011 |
| weight | 0.996 | 0.009 | -0.428 | 0.669 | 0.978 | 1.013 |
Note that all patients retained in this model. The patient (MRN 1001491594) with multiple types of discordance is therefore included but will be analyzed by whatever happened first. Specifically, this patient’s first instance of discordance was as follows: “2024-08-20 04:09:00 aPTT < antiXa”, thus is included in the aPTT < antiXa survival analysis.
All patients start “concordant” at ECMO cannulation (time_on).
If a patient ever becomes discordant, their first discordance time defines:
Patients who never become discordant are always in the “concordant” group and their covariates are sampled at ECMO start (time_on).
Events (bleeding and thrombosis) are followed until death or 7 days after ECMO stop, whichever comes first.
| first_discord_type | n |
|---|---|
| concordant | 77 |
| aPTT > antiXa | 17 |
| aPTT < antiXa | 19 |
| mrn | time_on | first_discord_time | first_discord_type | clot_first | censor_time |
|---|---|---|---|---|---|
| 1201199342 | 2024-09-09 17:56:00 | 2024-09-26 12:28:00 | aPTT > antiXa | 2024-09-23 22:16:00 | 2024-10-12 17:50:00 |
| mrn | time_on | first_discord_time | first_discord_type | bleed_first | censor_time |
|---|
| mrn | time_on | censor_time | first_discord_time | first_discord_type |
|---|
| event_throm | n |
|---|---|
| 0 | 69 |
| 1 | 44 |
| event_bleed | n |
|---|---|
| 0 | 106 |
| 1 | 7 |
## Ignoring unknown labels:
## • colour : "First discordance type"
## Ignoring unknown labels:
## • colour : "First discordance type"
###############################################################################
# Build time-varying data:
# - Time scale: days since ECMO cannulation (time_on)
# - Everyone starts concordant (0 -> t_disc)
# - If they ever discord, from t_disc onward they are in the discordant group
# - Events belong to whichever interval they occur in
###############################################################################
build_tvd <- function(df, event_time_var) {
empty_out <- tibble(
mrn = character(0),
start = numeric(0),
stop = numeric(0),
event = integer(0),
discord_group = factor(
character(0),
levels = c("concordant", "aPTT > antiXa", "aPTT < antiXa")
)
)
df %>%
rowwise() %>%
do({
row <- .
t0 <- row$time_on
t_end <- row$censor_time
t_disc <- row$first_discord_time
t_evt <- row[[event_time_var]]
if (is.na(t0) || is.na(t_end) || t_end <= t0) {
out <- empty_out
} else {
# is there an event within the window?
if (!is.na(t_evt) && t_evt <= t_end) {
has_event <- TRUE
t_final <- t_evt
} else {
has_event <- FALSE
t_final <- t_end
t_evt <- NA
}
# convert times to days from ECMO start
t_disc_days <- if (!is.na(t_disc)) as.numeric(difftime(t_disc, t0, units = "days")) else NA_real_
t_final_days <- as.numeric(difftime(t_final, t0, units = "days"))
evt_days <- if (!is.na(t_evt)) as.numeric(difftime(t_evt, t0, units = "days")) else NA_real_
# ensure disc time is slightly > 0 if extremely early
if (!is.na(t_disc_days)) {
min_gap_days <- 1/1440
t_disc_days <- max(t_disc_days, min_gap_days)
}
if (t_final_days <= 0) {
out <- empty_out
} else if (is.na(t_disc_days) || t_disc_days >= t_final_days) {
# never discordant OR discordance after follow-up ends
start1 <- 0
stop1 <- t_final_days
ev1 <- if (has_event) 1L else 0L
out <- tibble(
mrn = row$mrn,
start = start1,
stop = stop1,
event = ev1,
discord_group = factor(
"concordant",
levels = c("concordant", "aPTT > antiXa", "aPTT < antiXa")
)
)
} else {
# discordance occurs before final follow-up
if (has_event && !is.na(evt_days) && evt_days <= t_disc_days) {
# event occurs before discordance -> always concordant
start1 <- 0
stop1 <- evt_days
ev1 <- 1L
out <- tibble(
mrn = row$mrn,
start = start1,
stop = stop1,
event = ev1,
discord_group = factor(
"concordant",
levels = c("concordant", "aPTT > antiXa", "aPTT < antiXa")
)
)
} else {
# event after discordance or no event at all
start1 <- 0
stop1 <- t_disc_days
ev1 <- 0L
start2 <- t_disc_days
stop2 <- t_final_days
ev2 <- if (has_event) 1L else 0L
out <- tibble(
mrn = row$mrn,
start = c(start1, start2),
stop = c(stop1, stop2),
event = c(ev1, ev2),
discord_group = factor(
c("concordant", as.character(row$first_discord_type)),
levels = c("concordant", "aPTT > antiXa", "aPTT < antiXa")
)
)
}
}
}
out
}) %>%
ungroup() %>%
filter(!is.na(start), !is.na(stop), stop > start)
}
# Build TV datasets
tv_thrombosis <- build_tvd(surv_base, event_time_var = "clot_first")
tv_bleeding <- build_tvd(surv_base, event_time_var = "bleed_first")
# sanity: at time 0, everybody should be concordant
tv_thrombosis %>%
filter(start == 0) %>%
count(discord_group) %>%
knitr::kable(caption = "Time-varying thrombosis data: group membership at time 0")
| discord_group | n |
|---|---|
| concordant | 113 |
tv_bleeding %>%
filter(start == 0) %>%
count(discord_group) %>%
knitr::kable(caption = "Time-varying bleeding data: group membership at time 0")
| discord_group | n |
|---|---|
| concordant | 113 |
# --- Thrombosis (time-varying discordance) ---
fit_throm_tv <- survfit(
Surv(start, stop, event) ~ discord_group,
data = tv_thrombosis
)
ggsurvplot(
fit_throm_tv,
data = tv_thrombosis,
conf.int = FALSE,
break.time.by = 5,
xlab = "Days since ECMO cannulation",
title = "Time-Varying KM: Thrombosis",
risk.table = TRUE,
risk.table.height = 0.22,
risk.table.y.text.col = TRUE,
risk.table.y.text = FALSE,
risk.table.title = "Number at risk (time-varying exposure)",
tables.theme = theme_bw()
)
## Ignoring unknown labels:
## • colour : "Strata"
# --- Hemorrhage (time-varying discordance) ---
fit_bleed_tv <- survfit(
Surv(start, stop, event) ~ discord_group,
data = tv_bleeding
)
ggsurvplot(
fit_bleed_tv,
data = tv_bleeding,
conf.int = FALSE,
break.time.by = 5,
xlab = "Days since ECMO cannulation",
title = "Time-Varying KM: Bleeding",
risk.table = TRUE,
risk.table.height = 0.22,
risk.table.y.text.col = TRUE,
risk.table.y.text = FALSE,
risk.table.title = "Number at risk (time-varying exposure)",
tables.theme = theme_bw()
)
## Ignoring unknown labels:
## • colour : "Strata"
This is not a bug in your build_tvd() — it’s a quirk/limitation of how survfit() computes n.risk at exact cutpoints for multi-state/time-dependent strata (and then ggsurvplot() faithfully prints it).
In other words:
Your data: everyone concordant at time 0 ✅
survfit(summary, times=0): internally classifies some people as discordant at 0 ❌
Your plotted risk table: prints that internal n.risk ❌
Why survfit() does this
It’s almost always an endpoint convention problem with start–stop intervals at t = 0 combined with stratum assignment.
Even though your discordant intervals start at t_disc_days = 1 minute, survfit()’s internal machinery for n.risk at an exact timepoint can behave like:
include intervals with start <= t and include strata membership for subjects who have a later interval beginning “almost immediately”
OR it uses a subject-level “current stratum at t” routine that’s not robust at t=0 when subjects have multiple rows
The giveaway is that the “discordant-at-0” counts are smaller than the ever-discordant counts (8/10 vs 16/19). That screams: “some early-switchers are being classified as discordant at t=0 by the risk-set routine.”
###############################################################################
# Time-varying KM + CUSTOM (correct) risk table as ONE combined figure (HTML)
# - Computes risk counts from start/stop data: start <= t < stop, distinct MRN
# - Renders risk table as a ggplot and stacks under KM plot using patchwork
###############################################################################
library(dplyr)
library(tidyr)
library(ggplot2)
library(survival)
library(survminer)
library(patchwork)
#----------------------------
# 1) Build correct risk counts
#----------------------------
make_tv_risk_long <- function(tv, times = seq(0, 40, 5),
id = "mrn", strata = "discord_group",
start = "start", stop = "stop") {
lvls <- levels(tv[[strata]])
if (is.null(lvls)) lvls <- sort(unique(as.character(tv[[strata]])))
expand_grid(time = times, tmp_group = lvls) %>%
rowwise() %>%
mutate(
n_risk = {
t <- time
g <- tmp_group
tv %>%
filter(.data[[start]] <= t, .data[[stop]] > t, as.character(.data[[strata]]) == g) %>%
distinct(.data[[id]]) %>%
nrow()
}
) %>%
ungroup() %>%
transmute(
time = as.numeric(time),
discord_group = factor(tmp_group, levels = lvls),
n_risk = n_risk
)
}
#----------------------------
# 2) Risk table plot (ggplot)
#----------------------------
make_risk_table_plot <- function(risk_long,
times = sort(unique(risk_long$time)),
xlab = "Days since ECMO cannulation",
title = "Number at risk (time-varying exposure)",
text_size = 4) {
if (any(is.na(risk_long$discord_group))) {
stop("Risk table strata are NA. Check discord_group labels/levels.")
}
ggplot(risk_long, aes(x = time, y = discord_group)) +
geom_tile(fill = "white", color = "grey85", linewidth = 0.4, height = 0.95) +
geom_text(aes(label = n_risk), size = text_size) +
scale_x_continuous(breaks = times, limits = range(times)) +
labs(x = NULL, y = NULL, title = title) +
theme_bw(base_size = 12) +
theme(
plot.title = element_text(size = 13, face = "plain"),
axis.text.y = element_text(size = 11),
axis.text.x = element_text(size = 11),
panel.grid = element_blank(),
legend.position = "none",
plot.margin = margin(2, 5, 5, 5)
)
}
#----------------------------
# 3) One function to make the combined figure
#----------------------------
make_tv_km_with_custom_risktable <- function(tv, event_label = "Outcome",
times = seq(0, 40, 5),
xlab = "Days since ECMO cannulation",
km_height = 3.2, rt_height = 1.2,
pretty_labels = TRUE) {
# Safe label cleanup (no recode() dependency)
if (pretty_labels) {
tv <- tv %>%
mutate(discord_group = as.character(discord_group)) %>%
mutate(discord_group = case_when(
discord_group == "aPTT > antiXa" ~ "aPTT > anti-Xa",
discord_group == "aPTT < antiXa" ~ "aPTT < anti-Xa",
TRUE ~ discord_group
)) %>%
mutate(discord_group = factor(discord_group,
levels = c("concordant", "aPTT > anti-Xa", "aPTT < anti-Xa")))
}
# Fit KM (counting process) and make KM plot WITHOUT survminer risk table
fit_tv <- survfit(Surv(start, stop, event) ~ discord_group, data = tv)
# CLEAN LEGEND LABELS
names(fit_tv$strata) <- gsub("^discord_group=", "", names(fit_tv$strata))
g <- ggsurvplot(
fit_tv,
data = tv,
conf.int = FALSE,
break.time.by = diff(times)[1],
xlab = xlab,
title = paste0("Time-Varying KM: ", event_label),
risk.table = FALSE
)
km_plot <- g$plot +
scale_x_continuous(breaks = times, limits = range(times))
# Build correct risk table and plot it
risk_long <- make_tv_risk_long(tv, times = times)
rt_plot <- make_risk_table_plot(
risk_long,
times = times,
xlab = xlab,
title = "Number at risk (time-varying exposure)",
text_size = 4
) +
scale_x_continuous(breaks = times, limits = range(times))
(km_plot / rt_plot) + plot_layout(heights = c(km_height, rt_height))
}
#----------------------------
# 4) Run for BOTH outcomes (prints two combined figures)
#----------------------------
times <- seq(0, 40, 5)
# Thrombosis combined figure
make_tv_km_with_custom_risktable(
tv = tv_thrombosis,
event_label = "Thrombosis",
times = times,
xlab = "Days since ECMO cannulation"
)
## Scale for x is already present.
## Adding another scale for x, which will replace the existing scale.
## Scale for x is already present.
## Adding another scale for x, which will replace the existing scale.
## Warning: Removed 6 rows containing missing values or values outside the scale range
## (`geom_tile()`).
# Bleeding combined figure
make_tv_km_with_custom_risktable(
tv = tv_bleeding,
event_label = "Bleeding",
times = times,
xlab = "Days since ECMO cannulation"
)
## Scale for x is already present.
## Adding another scale for x, which will replace the existing scale.
## Scale for x is already present.
## Adding another scale for x, which will replace the existing scale.
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_step()`).
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_point()`).
## Warning: Removed 6 rows containing missing values or values outside the scale range
## (`geom_tile()`).
Here we:
###############################################################################
# 1. Index time per patient:
# - if discordant: first_discord_time
# - else (always concordant): first paired aPTT/anti-Xa lab_result_time
###############################################################################
# first paired aPTT/anti-Xa timestamp for everyone who has any paired labs
first_paired_coag_time <- model_fit_data %>%
group_by(mrn) %>%
summarise(first_paired_time = min(lab_result_time, na.rm = TRUE), .groups = "drop") %>%
mutate(first_paired_time = lubridate::force_tz(first_paired_time, "UTC"))
# define index time using discordance time if present, otherwise first paired time
index_times <- surv_base %>%
left_join(first_paired_coag_time, by = "mrn") %>%
transmute(
mrn,
index_time = dplyr::coalesce(first_discord_time, first_paired_time)
)
###############################################################################
# 2. Build a long covariate table using COVARIATE-SPECIFIC timestamps
# (inr_time, bilirubin_time, platelet_time, + dose_time)
###############################################################################
covar_long <- model_fit_complications %>%
select(
mrn,
lab_result_time,
weight,
nearest_inr, inr_time,
nearest_bilirubin, bilirubin_time,
nearest_platelet, platelet_time,
heparin_infusion_dose_value_units_kg_hr
) %>%
mutate(dose_time = lab_result_time) %>%
pivot_longer(
cols = c(nearest_inr, nearest_bilirubin, nearest_platelet, heparin_infusion_dose_value_units_kg_hr),
names_to = "covariate",
values_to = "value"
) %>%
mutate(
cov_time = case_when(
covariate == "nearest_inr" ~ inr_time,
covariate == "nearest_bilirubin" ~ bilirubin_time,
covariate == "nearest_platelet" ~ platelet_time,
covariate == "heparin_infusion_dose_value_units_kg_hr" ~ dose_time,
TRUE ~ as.POSIXct(NA)
)
) %>%
select(mrn, covariate, value, cov_time, weight) %>%
filter(!is.na(value), !is.na(cov_time)) %>%
distinct()
## Adding missing grouping variables: `lab_result_time`
###############################################################################
# 3. For each patient AND EACH covariate, select the nearest value to index_time
# (optional window; keep at 24h to match your design)
###############################################################################
covar_indexed_long <- covar_long %>%
inner_join(index_times, by = "mrn") %>%
mutate(time_diff_hours = abs(as.numeric(difftime(cov_time, index_time, units = "hours")))) %>%
filter(time_diff_hours <= 24) %>% # <-- adjust if you want
group_by(mrn, covariate) %>%
slice_min(time_diff_hours, with_ties = FALSE) %>%
ungroup()
###############################################################################
# 4. Wide covariate snapshot for Cox adjustment
###############################################################################
covar_snapshot <- covar_indexed_long %>%
select(mrn, covariate, value) %>%
pivot_wider(names_from = covariate, values_from = value) %>%
left_join(
covar_long %>% group_by(mrn) %>% summarise(weight = first(weight), .groups = "drop"),
by = "mrn"
)
cox_covars_indexed <- covar_snapshot %>%
select(
mrn,
nearest_inr,
nearest_bilirubin,
nearest_platelet,
heparin_infusion_dose_value_units_kg_hr,
weight
)
###############################################################################
# 5. Quick summary: how close were the matched covariates to index time?
###############################################################################
covar_indexed_long %>%
mutate(
covariate = recode(
covariate,
nearest_inr = "INR",
nearest_bilirubin = "Bilirubin",
nearest_platelet = "Platelet",
heparin_infusion_dose_value_units_kg_hr = "Heparin dose"
)
) %>%
group_by(covariate) %>%
summarise(
n_patients = n_distinct(mrn),
median_time_h = median(time_diff_hours, na.rm = TRUE),
p95_time_h = quantile(time_diff_hours, 0.95, na.rm = TRUE),
max_time_h = max(time_diff_hours, na.rm = TRUE),
.groups = "drop"
) %>%
knitr::kable(digits = 2, caption = "Time distance (hours) between index time and covariate values used for Cox adjustment")
| covariate | n_patients | median_time_h | p95_time_h | max_time_h |
|---|---|---|---|---|
| Bilirubin | 113 | 0.43 | 10.00 | 17.15 |
| Heparin dose | 113 | 0.00 | 0.00 | 0.00 |
| INR | 110 | 0.00 | 10.09 | 23.23 |
| Platelet | 112 | 0.46 | 4.38 | 10.63 |
###############################################################################
# Audit: MRNs where nearest covariate is far from index_time
###############################################################################
mrns_over_48h <- covar_long %>%
inner_join(index_times, by = "mrn") %>%
mutate(time_diff_hours = abs(as.numeric(difftime(cov_time, index_time, units = "hours")))) %>%
group_by(mrn, covariate) %>%
slice_min(time_diff_hours, with_ties = FALSE) %>%
ungroup() %>%
filter(time_diff_hours > 48) %>%
mutate(
covariate = recode(
covariate,
nearest_inr = "INR",
nearest_bilirubin = "Bilirubin",
nearest_platelet = "Platelet",
heparin_infusion_dose_value_units_kg_hr = "Heparin dose"
)
) %>%
arrange(desc(time_diff_hours))
# Wide view (one row per MRN)
mrns_over_48h %>%
select(mrn, covariate, time_diff_hours) %>%
pivot_wider(names_from = covariate, values_from = time_diff_hours) %>%
knitr::kable(digits = 2, caption = "Patients with covariate time gap > 48h (nearest per covariate to index_time)")
| mrn |
|---|
| discord_group | n |
|---|---|
| concordant | 109 |
| aPTT > antiXa | 15 |
| aPTT < antiXa | 18 |
| discord_group | n |
|---|---|
| concordant | 109 |
| aPTT > antiXa | 16 |
| aPTT < antiXa | 18 |
| term | std.error | robust.se | statistic | p.value | conf.low | conf.high | HR | p_fdr | p_holm | p_bonf |
|---|---|---|---|---|---|---|---|---|---|---|
| discord_groupaPTT > antiXa | 0.5435761 | 0.5182637 | -0.7585560 | 0.4481182 | 0.2444095 | 1.863854 | 0.6749396 | 0.4481182 | 0.8643128 | 0.8962364 |
| discord_groupaPTT < antiXa | 0.5409139 | 0.5615219 | -0.7855069 | 0.4321564 | 0.2140294 | 1.933793 | 0.6433416 | 0.4481182 | 0.8643128 | 0.8643128 |
| term | n_display | std.error | robust.se | statistic | p.value | conf.low | conf.high | n_patients | n_total | HR | p_fdr | p_holm | p_bonf |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| discord_groupaPTT > antiXa | NA / NA | 0.571 | 0.540 | -0.611 | 0.541 | 0.249 | 2.073 | NA | NA | 0.719 | 0.756 | 1 | 1 |
| discord_groupaPTT < antiXa | NA / NA | 0.571 | 0.607 | -0.457 | 0.648 | 0.230 | 2.491 | NA | NA | 0.758 | 0.756 | 1 | 1 |
| nearest_inr | 109 / 109 | 0.637 | 0.690 | 1.060 | 0.289 | 0.537 | 8.032 | 109 | 109 | 2.077 | 0.654 | 1 | 1 |
| nearest_bilirubin | 109 / 109 | 0.152 | 0.177 | -1.131 | 0.258 | 0.578 | 1.158 | 109 | 109 | 0.818 | 0.654 | 1 | 1 |
| nearest_platelet | 109 / 109 | 0.003 | 0.003 | 0.189 | 0.850 | 0.994 | 1.007 | 109 | 109 | 1.001 | 0.850 | 1 | 1 |
| heparin_infusion_dose_value_units_kg_hr | 109 / 109 | 0.039 | 0.042 | -1.006 | 0.314 | 0.883 | 1.041 | 109 | 109 | 0.959 | 0.654 | 1 | 1 |
| weight | 109 / 109 | 0.007 | 0.006 | -0.889 | 0.374 | 0.984 | 1.006 | 109 | 109 | 0.995 | 0.654 | 1 | 1 |
| term | std.error | robust.se | statistic | p.value | conf.low | conf.high | HR | p_fdr | p_holm | p_bonf |
|---|---|---|---|---|---|---|---|---|---|---|
| discord_groupaPTT > antiXa | 0.9309055 | 0.7986824 | 3.378016 | 0.0007301 | 3.1036329 | 71.04765 | 14.849439 | 0.0014602 | 0.0014602 | 0.0014602 |
| discord_groupaPTT < antiXa | 1.2589542 | 1.1919864 | 1.025546 | 0.3051058 | 0.3283044 | 35.11706 | 3.395451 | 0.3051058 | 0.3051058 | 0.6102116 |
| term | n_display | std.error | robust.se | statistic | p.value | conf.low | conf.high | n_patients | n_total | HR | p_fdr | p_holm | p_bonf |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| discord_groupaPTT > antiXa | NA / NA | 1.014 | 1.005 | 2.530 | 0.011 | 1.774 | 91.194 | NA | NA | 12.720 | 0.080 | 0.08 | 0.08 |
| discord_groupaPTT < antiXa | NA / NA | 1.394 | 1.294 | 0.673 | 0.501 | 0.189 | 30.140 | NA | NA | 2.388 | 0.827 | 1.00 | 1.00 |
| nearest_inr | 109 / 109 | 1.673 | 1.542 | 0.218 | 0.827 | 0.068 | 28.721 | 109 | 109 | 1.399 | 0.827 | 1.00 | 1.00 |
| nearest_bilirubin | 109 / 109 | 0.302 | 0.233 | 0.275 | 0.784 | 0.675 | 1.684 | 109 | 109 | 1.066 | 0.827 | 1.00 | 1.00 |
| nearest_platelet | 109 / 109 | 0.009 | 0.008 | 0.523 | 0.601 | 0.989 | 1.020 | 109 | 109 | 1.004 | 0.827 | 1.00 | 1.00 |
| heparin_infusion_dose_value_units_kg_hr | 109 / 109 | 0.084 | 0.067 | 1.098 | 0.272 | 0.944 | 1.228 | 109 | 109 | 1.076 | 0.827 | 1.00 | 1.00 |
| weight | 109 / 109 | 0.023 | 0.019 | -0.558 | 0.577 | 0.954 | 1.027 | 109 | 109 | 0.990 | 0.827 | 1.00 | 1.00 |
## # A tibble: 4 × 14
## term n_display std.error robust.se statistic p.value conf.low conf.high
## <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 discord_gr… NA / NA 1.00 0.987 2.91 0.00367 2.54 122.
## 2 discord_gr… NA / NA 1.28 1.27 0.813 0.416 0.234 33.5
## 3 nearest_inr 109 / 109 1.57 1.95 0.142 0.887 0.0291 59.8
## 4 nearest_pl… 109 / 109 0.00769 0.00765 0.818 0.414 0.991 1.02
## # ℹ 6 more variables: n_patients <int>, n_total <int>, HR <dbl>, p_fdr <dbl>,
## # p_holm <dbl>, p_bonf <dbl>
Notes: 109 patients in CPH models (of 113 total patients) - missing INR = 3 - missing platelet = 1
“Robust standard errors clustered by patient were used to account for within-patient correlation.”
Limitations include few hemorrhage events. Thus, risk of type 1 error when interpreting individual terms. Hazard ratios are imprecise
*** working ***
ph_test_univariable_thromb <- cox.zph(cox_thrombosis_univariable)
ph_test_univariable_bleed <- cox.zph(cox_bleed_univariable)
ph_test_thromb <- cox.zph(cox_thromb_indexed)
ph_test_bleed <- cox.zph(cox_bleed_indexed)
ph_test_univariable_thromb
## chisq df p
## discord_group 0.597 2 0.74
## GLOBAL 0.597 2 0.74
ph_test_univariable_bleed
## chisq df p
## discord_group 0.883 2 0.64
## GLOBAL 0.883 2 0.64
ph_test_thromb
## chisq df p
## discord_group 0.4339 2 0.80
## nearest_inr 0.3523 1 0.55
## nearest_bilirubin 0.0423 1 0.84
## nearest_platelet 0.5868 1 0.44
## heparin_infusion_dose_value_units_kg_hr 0.2814 1 0.60
## weight 0.3902 1 0.53
## GLOBAL 2.1700 7 0.95
ph_test_bleed
## chisq df p
## discord_group 0.945 2 0.623
## nearest_inr 4.427 1 0.035
## nearest_bilirubin 2.228 1 0.136
## nearest_platelet 1.849 1 0.174
## heparin_infusion_dose_value_units_kg_hr 0.617 1 0.432
## weight 0.509 1 0.476
## GLOBAL 8.151 7 0.319
plot(ph_test_univariable_thromb)
plot(ph_test_univariable_bleed)
plot(ph_test_thromb)
plot(ph_test_bleed)
How to interpret (exploratory framing)
Global test p > 0.05 → no strong evidence PH violated
Term-level p < 0.05 → possible time-varying effect
Mild violations are acceptable in exploratory analyses if acknowledged
No strong global violations of the proportional hazards assumption were observed with either model as assessed with Schoenfeld residuals. In the hemorrhage model, INR demonstrated mild non-proportionally (chi square 4.427, p = 0.035), hazard ratios should therefore be interpreted for this covariable as time-averaged effects.
(i.e. INR impact is a blend of effects over time, and is not always equal throughout the time on ECMO)
need to fit model without clustering model with max covariables checked
library(car)
## Loading required package: carData
##
## Attaching package: 'car'
## The following object is masked from 'package:dplyr':
##
## recode
## The following object is masked from 'package:purrr':
##
## some
# thrombosis
cox_thrombosis_nocluster <- coxph(
Surv(start, stop, event) ~
discord_group +
nearest_inr +
nearest_bilirubin +
nearest_platelet +
heparin_infusion_dose_value_units_kg_hr +
weight,
data = tv_thrombosis_indexed)
vif(cox_thrombosis_nocluster)
## Warning in vif.default(cox_thrombosis_nocluster): No intercept: vifs may not be
## sensible.
## GVIF Df GVIF^(1/(2*Df))
## discord_group 1.249233 2 1.057209
## nearest_inr 1.206933 1 1.098605
## nearest_bilirubin 1.257274 1 1.121282
## nearest_platelet 1.206480 1 1.098399
## heparin_infusion_dose_value_units_kg_hr 1.142991 1 1.069108
## weight 1.137082 1 1.066340
# hemorrhage
cox_bleed_nocluster <- coxph(
Surv(start, stop, event) ~
discord_group +
nearest_inr +
nearest_bilirubin +
nearest_platelet +
heparin_infusion_dose_value_units_kg_hr +
weight,
data = tv_bleeding_indexed)
vif(cox_bleed_nocluster)
## Warning in vif.default(cox_bleed_nocluster): No intercept: vifs may not be
## sensible.
## GVIF Df GVIF^(1/(2*Df))
## discord_group 1.670524 2 1.136876
## nearest_inr 1.365006 1 1.168335
## nearest_bilirubin 1.496136 1 1.223166
## nearest_platelet 1.658544 1 1.287845
## heparin_infusion_dose_value_units_kg_hr 1.478066 1 1.215757
## weight 1.440772 1 1.200322
VIF ≈ 1–2 → no concern
# thrombosis
dfb <- residuals(cox_thromb_indexed, type = "dfbeta")
# Max influence per term
apply(abs(dfb), 2, max)
## [1] 0.264947402 0.273640069 0.386392297 0.095695415 0.001307056 0.016792951
## [7] 0.001938324
matplot(abs(dfb), type = "l", lty = 1,
ylab = "|DFBETA|", xlab = "Observation index")
abline(h = 0.5, col = "red", lty = 2)
# hemorrhage
dfb <- residuals(cox_bleed_indexed, type = "dfbeta")
# Max influence per term
apply(abs(dfb), 2, max)
## [1] 0.585135025 1.029875644 0.891290457 0.179056666 0.003483407 0.039208687
## [7] 0.010884140
matplot(abs(dfb), type = "l", lty = 1,
ylab = "|DFBETA|", xlab = "Observation index")
abline(h = 0.5, col = "red", lty = 2)
Interpretation
|DFBETA| > ~0.5 suggests influential individuals
# thrombosis
mart <- residuals(cox_thromb_indexed, type = "martingale")
plot(tv_thrombosis_indexed$nearest_inr, mart)
loess_fit <- loess(mart ~ tv_thrombosis_indexed$nearest_inr)
lines(tv_thrombosis_indexed$nearest_inr, fitted(loess_fit), col="red")
plot(tv_thrombosis_indexed$nearest_bilirubin, mart)
loess_fit <- loess(mart ~ tv_thrombosis_indexed$nearest_bilirubin)
lines(tv_thrombosis_indexed$nearest_bilirubin, fitted(loess_fit), col="red")
plot(tv_thrombosis_indexed$nearest_platelet, mart)
loess_fit <- loess(mart ~ tv_thrombosis_indexed$nearest_platelet)
lines(tv_thrombosis_indexed$nearest_platelet, fitted(loess_fit), col="red")
plot(tv_thrombosis_indexed$heparin_infusion_dose_value_units_kg_hr, mart)
loess_fit <- loess(mart ~ tv_thrombosis_indexed$heparin_infusion_dose_value_units_kg_hr)
lines(tv_thrombosis_indexed$heparin_infusion_dose_value_units_kg_hr, fitted(loess_fit), col="red")
plot(tv_thrombosis_indexed$weight, mart)
loess_fit <- loess(mart ~ tv_thrombosis_indexed$weight)
lines(tv_thrombosis_indexed$weight, fitted(loess_fit), col="red")
plot(tv_thrombosis_indexed$heparin_infusion_dose_value_units_kg_hr, mart)
loess_fit <- loess(mart ~ tv_thrombosis_indexed$heparin_infusion_dose_value_units_kg_hr)
lines(tv_thrombosis_indexed$heparin_infusion_dose_value_units_kg_hr, fitted(loess_fit), col="red")
# hemorrhage
mart <- residuals(cox_bleed_indexed, type = "martingale")
plot(tv_bleeding_indexed$nearest_inr, mart)
loess_fit <- loess(mart ~ tv_bleeding_indexed$nearest_inr)
lines(tv_bleeding_indexed$nearest_inr, fitted(loess_fit), col="red")
plot(tv_bleeding_indexed$nearest_bilirubin, mart)
loess_fit <- loess(mart ~ tv_bleeding_indexed$nearest_bilirubin)
lines(tv_bleeding_indexed$nearest_bilirubin, fitted(loess_fit), col="red")
plot(tv_bleeding_indexed$nearest_platelet, mart)
loess_fit <- loess(mart ~ tv_bleeding_indexed$nearest_platelet)
lines(tv_bleeding_indexed$nearest_platelet, fitted(loess_fit), col="red")
plot(tv_bleeding_indexed$heparin_infusion_dose_value_units_kg_hr, mart)
loess_fit <- loess(mart ~ tv_bleeding_indexed$heparin_infusion_dose_value_units_kg_hr)
lines(tv_bleeding_indexed$heparin_infusion_dose_value_units_kg_hr, fitted(loess_fit), col="red")
plot(tv_bleeding_indexed$weight, mart)
loess_fit <- loess(mart ~ tv_bleeding_indexed$weight)
lines(tv_bleeding_indexed$weight, fitted(loess_fit), col="red")
plot(tv_bleeding_indexed$heparin_infusion_dose_value_units_kg_hr, mart)
loess_fit <- loess(mart ~ tv_bleeding_indexed$heparin_infusion_dose_value_units_kg_hr)
lines(tv_bleeding_indexed$heparin_infusion_dose_value_units_kg_hr, fitted(loess_fit), col="red")
# thrombosis
n_events <- sum(tv_thrombosis_indexed$event)
n_vars <- length(coef(cox_thromb_indexed))
n_events / n_vars
## [1] 5.857143
# bleeding
n_events <- sum(tv_bleeding_indexed$event)
n_vars <- length(coef(cox_bleed_indexed))
n_events / n_vars
## [1] 1
Limitation: we have relatively few bleeding events. Thus hazard ratios are imprecise and should be interpreted cautiously.
death_date
mechanical_circuit_change
renal_creatinine_3_0
renal_creatinine_1_5_3_0
renal_renal_replacement_therapy_required
metabolic_moderate_hemolysis
major_hemolysis_perfusion
Note: composited renal failure types into
renal_failure_any
Ultimately there are very few of these outcomes (except renal failure and death).
Note that this analysis eliminated 1 lab results in which aPTT was above typical goal rangs and time-matched anti-Xa was below typical goal range and 1 lab result in which aPTT was below typical goal range and time-matched anti-Xa was above typical goal range.
## # A tibble: 5 × 2
## # Groups: concordance_classic_simple [5]
## concordance_classic_simple n
## <fct> <int>
## 1 concordant 379
## 2 discordant aPTT below antiXa 148
## 3 discordant aPTT above antiXa 42
## 4 high aPTT low antiXa 1
## 5 low aPTT high antiXa 1
## # A tibble: 5 × 2
## # Groups: concordance_low_simple [5]
## concordance_low_simple n
## <fct> <int>
## 1 concordant 327
## 2 discordant aPTT below antiXa 183
## 3 discordant aPTT above antiXa 59
## 4 high aPTT low antiXa 1
## 5 low aPTT high antiXa 1
##
## Counts (Classic):
## # A tibble: 2 × 2
## classic_discordant n
## <lgl> <int>
## 1 FALSE 49
## 2 TRUE 63
##
## Counts (Modeled):
## # A tibble: 2 × 2
## modeled_discordant n
## <lgl> <int>
## 1 FALSE 77
## 2 TRUE 35
##
## Classic vs Modeled Discordance Table:
## modeled
## classic FALSE TRUE
## FALSE 43 6
## TRUE 34 29
## Warning in tidy.table(.): 'tidy.table' is deprecated.
## Use 'tibble::as_tibble()' instead.
## See help("Deprecated")
## # A tibble: 4 × 3
## classic modeled n
## <chr> <chr> <int>
## 1 FALSE FALSE 43
## 2 TRUE FALSE 34
## 3 FALSE TRUE 6
## 4 TRUE TRUE 29
##
## McNemar Test:
##
## McNemar's Chi-squared test with continuity correction
##
## data: discord_table
## McNemar's chi-squared = 18.225, df = 1, p-value = 1.963e-05
## # A tibble: 1 × 4
## statistic p.value parameter method
## <dbl> <dbl> <dbl> <chr>
## 1 18.2 0.0000196 1 McNemar's Chi-squared test with continuity corr…
## === 3×3 Classification Table (Classic vs Polynomial Model) ===
## modeled
## classic aPTT < antiXa aPTT > antiXa concordant
## aPTT < antiXa 14 4 30
## aPTT > antiXa 0 11 4
## concordant 4 2 43
##
## === Chi-square Test ===
##
## Pearson's Chi-squared test with simulated p-value (based on 5000
## replicates)
##
## data: discord_table_full
## X-squared = 55.029, df = NA, p-value = 2e-04
##
## === Discordant-Only 2×2 Table (Low vs High) ===
## modeled
## classic aPTT < antiXa aPTT > antiXa concordant
## aPTT < antiXa 14 4 30
## aPTT > antiXa 0 11 4
## concordant 4 2 0
## Warning in tidy.table(.): 'tidy.table' is deprecated.
## Use 'tibble::as_tibble()' instead.
## See help("Deprecated")
## # A tibble: 9 × 3
## classic modeled n
## <chr> <chr> <int>
## 1 aPTT < antiXa aPTT < antiXa 14
## 2 aPTT > antiXa aPTT < antiXa 0
## 3 concordant aPTT < antiXa 4
## 4 aPTT < antiXa aPTT > antiXa 4
## 5 aPTT > antiXa aPTT > antiXa 11
## 6 concordant aPTT > antiXa 2
## 7 aPTT < antiXa concordant 30
## 8 aPTT > antiXa concordant 4
## 9 concordant concordant 0
##
## === Chi-square Test (Discordant Types Only) ===
##
## Pearson's Chi-squared test with simulated p-value (based on 5000
## replicates)
##
## data: discord_types
## X-squared = 33.891, df = NA, p-value = 2e-04
## # A tibble: 1 × 4
## statistic p.value parameter method
## <dbl> <dbl> <lgl> <chr>
## 1 33.9 0.000200 NA "Pearson's Chi-squared test with simulated p-val…
Note that this analysis eliminated 1 lab results in which aPTT was above typical goal range and time-matched anti-Xa was below typical goal range and 1 lab result in which aPTT was below typical goal range and time-matched anti-Xa was above typical goal range.
##
## =============================
## Category: aPTT < antiXa
## =============================
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 48 20 0.417
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 18 4 0.222
##
## 2×2 Thrombosis Table:
## thrombosis no_thrombosis
## classic 20 28
## modeled 4 14
##
## Fisher Test:
##
## Fisher's Exact Test for Count Data
##
## data: table_mat
## p-value = 0.1649
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.6438088 11.8457795
## sample estimates:
## odds ratio
## 2.467387
##
##
##
##
## =============================
## Category: aPTT > antiXa
## =============================
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 15 6 0.4
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 17 6 0.353
##
## 2×2 Thrombosis Table:
## thrombosis no_thrombosis
## classic 6 9
## modeled 6 11
##
## Fisher Test:
##
## Fisher's Exact Test for Count Data
##
## data: table_mat
## p-value = 1
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.2311049 6.4469852
## sample estimates:
## odds ratio
## 1.214552
##
##
##
##
## =============================
## Category: concordant
## =============================
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 49 18 0.367
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 77 34 0.442
##
## 2×2 Thrombosis Table:
## thrombosis no_thrombosis
## classic 18 31
## modeled 34 43
##
## Fisher Test:
##
## Fisher's Exact Test for Count Data
##
## data: table_mat
## p-value = 0.4607
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.3280859 1.6249909
## sample estimates:
## odds ratio
## 0.7361483
##
## =============================
## Category: aPTT < antiXa
## =============================
## # A tibble: 1 × 3
## n hemorrhage_n hemorrhage_rate
## <int> <int> <dbl>
## 1 48 2 0.0417
## # A tibble: 1 × 3
## n hemorrhage_n hemorrhage_rate
## <int> <int> <dbl>
## 1 18 1 0.0556
##
## 2×2 Hemorrhage Table:
## hemorrhage no_hemorrhage
## classic 2 46
## modeled 1 17
##
## Fisher Test:
##
## Fisher's Exact Test for Count Data
##
## data: table_mat
## p-value = 1
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.03641973 46.14255757
## sample estimates:
## odds ratio
## 0.74273
##
##
##
##
## =============================
## Category: aPTT > antiXa
## =============================
## # A tibble: 1 × 3
## n hemorrhage_n hemorrhage_rate
## <int> <int> <dbl>
## 1 15 3 0.2
## # A tibble: 1 × 3
## n hemorrhage_n hemorrhage_rate
## <int> <int> <dbl>
## 1 17 4 0.235
##
## 2×2 Hemorrhage Table:
## hemorrhage no_hemorrhage
## classic 3 12
## modeled 4 13
##
## Fisher Test:
##
## Fisher's Exact Test for Count Data
##
## data: table_mat
## p-value = 1
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.09859548 5.98490546
## sample estimates:
## odds ratio
## 0.817771
##
##
##
##
## =============================
## Category: concordant
## =============================
## # A tibble: 1 × 3
## n hemorrhage_n hemorrhage_rate
## <int> <int> <dbl>
## 1 49 3 0.0612
## # A tibble: 1 × 3
## n hemorrhage_n hemorrhage_rate
## <int> <int> <dbl>
## 1 77 3 0.0390
##
## 2×2 Hemorrhage Table:
## hemorrhage no_hemorrhage
## classic 3 46
## modeled 3 74
##
## Fisher Test:
##
## Fisher's Exact Test for Count Data
##
## data: table_mat
## p-value = 0.6769
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.2058079 12.4748253
## sample estimates:
## odds ratio
## 1.602364
##
## =============================
## Low Range vs Polynomial — Thrombosis
## Category: aPTT < antiXa
## =============================
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 65 24 0.369
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 18 4 0.222
##
## 2×2 Table:
## thrombosis no_thrombosis
## low_range 24 41
## modeled 4 14
##
## Fisher Test:
##
## Fisher's Exact Test for Count Data
##
## data: table_mat
## p-value = 0.2766
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.5519251 9.4577870
## sample estimates:
## odds ratio
## 2.032359
##
##
##
##
## =============================
## Low Range vs Polynomial — Thrombosis
## Category: aPTT > antiXa
## =============================
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 19 8 0.421
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 17 6 0.353
##
## 2×2 Table:
## thrombosis no_thrombosis
## low_range 8 11
## modeled 6 11
##
## Fisher Test:
##
## Fisher's Exact Test for Count Data
##
## data: table_mat
## p-value = 0.7419
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.2855128 6.3885568
## sample estimates:
## odds ratio
## 1.322697
##
##
##
##
## =============================
## Low Range vs Polynomial — Thrombosis
## Category: concordant
## =============================
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 28 12 0.429
## # A tibble: 1 × 3
## n thromb_n thromb_rate
## <int> <int> <dbl>
## 1 77 34 0.442
##
## 2×2 Table:
## thrombosis no_thrombosis
## low_range 12 16
## modeled 34 43
##
## Fisher Test:
##
## Fisher's Exact Test for Count Data
##
## data: table_mat
## p-value = 1
## alternative hypothesis: true odds ratio is not equal to 1
## 95 percent confidence interval:
## 0.3573489 2.4714823
## sample estimates:
## odds ratio
## 0.9489871
## # A tibble: 3 × 4
## unusual_type n hemorrhage_rate thrombosis_rate
## <fct> <int> <dbl> <dbl>
## 1 concordant 477 7.13 29.8
## 2 aPTT > antiXa 36 11.1 25
## 3 aPTT < antiXa 58 1.72 24.1
##
## Call: glm(formula = hemorrhage_any ~ unusual_type, family = binomial,
## data = model_fit_complications)
##
## Coefficients:
## (Intercept) unusual_typeaPTT > antiXa
## -2.5672 0.4878
## unusual_typeaPTT < antiXa
## -1.4758
##
## Degrees of Freedom: 570 Total (i.e. Null); 568 Residual
## Null Deviance: 284.6
## Residual Deviance: 280.3 AIC: 286.3
##
## Call:
## glm(formula = hemorrhage_any ~ unusual_type, family = binomial,
## data = model_fit_complications)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -2.5672 0.1780 -14.426 <2e-16 ***
## unusual_typeaPTT > antiXa 0.4878 0.5594 0.872 0.383
## unusual_typeaPTT < antiXa -1.4758 1.0241 -1.441 0.150
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 284.61 on 570 degrees of freedom
## Residual deviance: 280.33 on 568 degrees of freedom
## AIC: 286.33
##
## Number of Fisher Scoring iterations: 6
##
## Call: glm(formula = thrombosis_any ~ unusual_type, family = binomial,
## data = model_fit_complications)
##
## Coefficients:
## (Intercept) unusual_typeaPTT > antiXa
## -0.8583 -0.2403
## unusual_typeaPTT < antiXa
## -0.2868
##
## Degrees of Freedom: 570 Total (i.e. Null); 568 Residual
## Null Deviance: 686.6
## Residual Deviance: 685.5 AIC: 691.5
##
## Call:
## glm(formula = thrombosis_any ~ unusual_type, family = binomial,
## data = model_fit_complications)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.8583 0.1001 -8.571 <2e-16 ***
## unusual_typeaPTT > antiXa -0.2403 0.3977 -0.604 0.546
## unusual_typeaPTT < antiXa -0.2868 0.3228 -0.889 0.374
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 686.60 on 570 degrees of freedom
## Residual deviance: 685.49 on 568 degrees of freedom
## AIC: 691.49
##
## Number of Fisher Scoring iterations: 4
##
## Call:
## glm(formula = hemorrhage_any ~ unusual_type + duration_to_lab +
## international_normalization_ratio + fibrinogen + nearest_bilirubin +
## weight + heparin_infusion_dose_value_units_kg_hr, family = binomial,
## data = model_fit_complications)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 5.437e-01 3.189e+00 0.170 0.86462
## unusual_typeaPTT > antiXa -1.629e+01 1.751e+03 -0.009 0.99258
## unusual_typeaPTT < antiXa -4.178e-01 1.301e+00 -0.321 0.74816
## duration_to_lab -1.206e-03 4.098e-03 -0.294 0.76857
## international_normalization_ratio 1.358e+00 2.296e+00 0.592 0.55404
## fibrinogen 2.194e-05 1.954e-03 0.011 0.99104
## nearest_bilirubin -4.498e-01 4.408e-01 -1.020 0.30753
## weight -6.816e-02 2.174e-02 -3.135 0.00172
## heparin_infusion_dose_value_units_kg_hr 1.732e-01 9.296e-02 1.863 0.06248
##
## (Intercept)
## unusual_typeaPTT > antiXa
## unusual_typeaPTT < antiXa
## duration_to_lab
## international_normalization_ratio
## fibrinogen
## nearest_bilirubin
## weight **
## heparin_infusion_dose_value_units_kg_hr .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 86.459 on 167 degrees of freedom
## Residual deviance: 61.610 on 159 degrees of freedom
## (403 observations deleted due to missingness)
## AIC: 79.61
##
## Number of Fisher Scoring iterations: 16
| GVIF | Df | GVIF^(1/(2*Df)) | |
|---|---|---|---|
| unusual_type | 1.355896 | 2 | 1.079087 |
| duration_to_lab | 1.755864 | 1 | 1.325090 |
| international_normalization_ratio | 1.304064 | 1 | 1.141956 |
| fibrinogen | 1.407007 | 1 | 1.186173 |
| nearest_bilirubin | 1.147931 | 1 | 1.071416 |
| weight | 1.595967 | 1 | 1.263316 |
| heparin_infusion_dose_value_units_kg_hr | 1.836584 | 1 | 1.355206 |
## # A tibble: 9 × 8
## term OR conf.low conf.high p_raw p_fdr p_holm p_bonf
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 1.72e+0 0.00588 2.22e+ 3 0.865 0.993 1 1
## 2 unusual_typeaPTT > an… 8.44e-8 NA 7.69e+56 0.993 0.993 1 1
## 3 unusual_typeaPTT < an… 6.59e-1 0.0276 7.14e+ 0 0.748 0.993 1 1
## 4 duration_to_lab 9.99e-1 0.990 1.01e+ 0 0.769 0.993 1 1
## 5 international_normali… 3.89e+0 0.0134 1.70e+ 2 0.554 0.993 1 1
## 6 fibrinogen 1.00e+0 0.996 1.00e+ 0 0.991 0.993 1 1
## 7 nearest_bilirubin 6.38e-1 0.209 1.21e+ 0 0.308 0.923 1 1
## 8 weight 9.34e-1 0.890 9.71e- 1 0.00172 0.0155 0.0155 0.0155
## 9 heparin_infusion_dose… 1.19e+0 1.00 1.45e+ 0 0.0625 0.281 0.500 0.562
## [1] TRUE
##
## FALSE TRUE
## concordant 443 34
## aPTT > antiXa 32 4
## aPTT < antiXa 57 1
| term | estimate | OR | conf.low | conf.high | p_raw | p_fdr | p_holm | p_bonf |
|---|---|---|---|---|---|---|---|---|
| (Intercept) | -0.4517335 | 0.6365238 | 0.0055107 | 394.8943525 | 0.8573556 | 0.9645251 | 1.0000000 | 1.0000000 |
| unusual_typeaPTT > antiXa | -1.1943994 | 0.3028858 | 0.0019908 | 4.7298196 | 0.4328422 | 0.8319527 | 1.0000000 | 1.0000000 |
| unusual_typeaPTT < antiXa | -0.2020069 | 0.8170893 | 0.0680678 | 6.1958257 | 0.8512867 | 0.9645251 | 1.0000000 | 1.0000000 |
| duration_to_lab | -0.0008927 | 0.9991077 | 0.9914045 | 1.0066575 | 0.8082157 | 0.9645251 | 1.0000000 | 1.0000000 |
| international_normalization_ratio | 1.7131475 | 5.5463913 | 0.0361873 | 90.6128738 | 0.3617986 | 0.8319527 | 1.0000000 | 1.0000000 |
| fibrinogen | -0.0000540 | 0.9999460 | 0.9962827 | 1.0036019 | 0.9762417 | 0.9762417 | 1.0000000 | 1.0000000 |
| nearest_bilirubin | -0.2232760 | 0.7998941 | 0.3003558 | 1.2897960 | 0.4621959 | 0.8319527 | 1.0000000 | 1.0000000 |
| weight | -0.0575024 | 0.9441196 | 0.9050462 | 0.9775113 | 0.0009199 | 0.0082792 | 0.0082792 | 0.0082792 |
| heparin_infusion_dose_value_units_kg_hr | 0.1432434 | 1.1540107 | 0.9887063 | 1.3823250 | 0.0701087 | 0.3154892 | 0.5608697 | 0.6309784 |
##
## Call:
## glm(formula = thrombosis_any ~ unusual_type + international_normalization_ratio +
## heparin_infusion_dose_value_units_kg_hr + duration_to_lab +
## fibrinogen + nearest_bilirubin + weight, family = binomial,
## data = model_fit_complications)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 4.201e-01 1.858e+00 0.226 0.821126
## unusual_typeaPTT > antiXa -1.488e+01 1.056e+03 -0.014 0.988756
## unusual_typeaPTT < antiXa 1.348e+00 5.398e-01 2.497 0.012509
## international_normalization_ratio -5.654e-01 1.228e+00 -0.460 0.645206
## heparin_infusion_dose_value_units_kg_hr -2.186e-01 6.284e-02 -3.478 0.000505
## duration_to_lab -7.959e-03 3.629e-03 -2.193 0.028276
## fibrinogen -8.438e-04 9.307e-04 -0.907 0.364581
## nearest_bilirubin 2.562e-02 1.347e-01 0.190 0.849085
## weight 2.474e-02 1.010e-02 2.449 0.014327
##
## (Intercept)
## unusual_typeaPTT > antiXa
## unusual_typeaPTT < antiXa *
## international_normalization_ratio
## heparin_infusion_dose_value_units_kg_hr ***
## duration_to_lab *
## fibrinogen
## nearest_bilirubin
## weight *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 202.82 on 167 degrees of freedom
## Residual deviance: 162.36 on 159 degrees of freedom
## (403 observations deleted due to missingness)
## AIC: 180.36
##
## Number of Fisher Scoring iterations: 15
## # A tibble: 9 × 7
## term estimate std.error statistic p.value conf.low conf.high
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 1.52e+0 1.86e+0 0.226 8.21e-1 0.0413 7.45e+ 1
## 2 unusual_typeaPTT > an… 3.44e-7 1.06e+3 -0.0141 9.89e-1 NA 3.54e+40
## 3 unusual_typeaPTT < an… 3.85e+0 5.40e-1 2.50 1.25e-2 1.37 1.16e+ 1
## 4 international_normali… 5.68e-1 1.23e+0 -0.460 6.45e-1 0.0384 6.00e+ 0
## 5 heparin_infusion_dose… 8.04e-1 6.28e-2 -3.48 5.05e-4 0.705 9.03e- 1
## 6 duration_to_lab 9.92e-1 3.63e-3 -2.19 2.83e-2 0.985 9.99e- 1
## 7 fibrinogen 9.99e-1 9.31e-4 -0.907 3.65e-1 0.997 1.00e+ 0
## 8 nearest_bilirubin 1.03e+0 1.35e-1 0.190 8.49e-1 0.769 1.32e+ 0
## 9 weight 1.03e+0 1.01e-2 2.45 1.43e-2 1.01 1.05e+ 0
| GVIF | Df | GVIF^(1/(2*Df)) | |
|---|---|---|---|
| unusual_type | 1.258849 | 2 | 1.059238 |
| international_normalization_ratio | 1.408611 | 1 | 1.186849 |
| heparin_infusion_dose_value_units_kg_hr | 1.503484 | 1 | 1.226166 |
| duration_to_lab | 1.323139 | 1 | 1.150278 |
| fibrinogen | 1.322245 | 1 | 1.149889 |
| nearest_bilirubin | 1.418127 | 1 | 1.190851 |
| weight | 1.346531 | 1 | 1.160401 |
## # A tibble: 9 × 8
## term OR conf.low conf.high p_raw p_fdr p_holm p_bonf
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 1.52e+0 0.0413 7.45e+ 1 8.21e-1 0.955 1 1
## 2 unusual_typeaPTT >… 3.44e-7 NA 3.54e+40 9.89e-1 0.989 1 1
## 3 unusual_typeaPTT <… 3.85e+0 1.37 1.16e+ 1 1.25e-2 0.0430 0.100 0.113
## 4 international_norm… 5.68e-1 0.0384 6.00e+ 0 6.45e-1 0.955 1 1
## 5 heparin_infusion_d… 8.04e-1 0.705 9.03e- 1 5.05e-4 0.00454 0.00454 0.00454
## 6 duration_to_lab 9.92e-1 0.985 9.99e- 1 2.83e-2 0.0636 0.170 0.254
## 7 fibrinogen 9.99e-1 0.997 1.00e+ 0 3.65e-1 0.656 1 1
## 8 nearest_bilirubin 1.03e+0 0.769 1.32e+ 0 8.49e-1 0.955 1 1
## 9 weight 1.03e+0 1.01 1.05e+ 0 1.43e-2 0.0430 0.100 0.129
## [1] TRUE
##
## FALSE TRUE
## concordant 335 142
## aPTT > antiXa 27 9
## aPTT < antiXa 44 14
| term | estimate | OR | conf.low | conf.high | p_raw | p_fdr | p_holm | p_bonf |
|---|---|---|---|---|---|---|---|---|
| (Intercept) | 0.3112238 | 1.3650946 | 0.0489856 | 50.5729181 | 0.8542002 | 0.8542002 | 1.0000000 | 1.0000000 |
| unusual_typeaPTT > antiXa | -0.8812509 | 0.4142644 | 0.0029282 | 5.0395611 | 0.5416406 | 0.8124609 | 1.0000000 | 1.0000000 |
| unusual_typeaPTT < antiXa | 1.2449190 | 3.4726536 | 1.2893237 | 9.9483446 | 0.0136282 | 0.0408845 | 0.0953973 | 0.1226536 |
| duration_to_lab | -0.0073757 | 0.9926514 | 0.9854777 | 0.9991030 | 0.0247555 | 0.0556998 | 0.1485328 | 0.2227992 |
| international_normalization_ratio | -0.4233769 | 0.6548318 | 0.0549153 | 5.3981260 | 0.6915597 | 0.8542002 | 1.0000000 | 1.0000000 |
| fibrinogen | -0.0008607 | 0.9991397 | 0.9973761 | 1.0009172 | 0.3391874 | 0.6105373 | 1.0000000 | 1.0000000 |
| nearest_bilirubin | 0.0386472 | 1.0394037 | 0.7886354 | 1.3062943 | 0.7627375 | 0.8542002 | 1.0000000 | 1.0000000 |
| weight | 0.0223096 | 1.0225603 | 1.0054031 | 1.0444797 | 0.0081549 | 0.0366972 | 0.0652394 | 0.0733944 |
| heparin_infusion_dose_value_units_kg_hr | -0.1996967 | 0.8189791 | 0.7221164 | 0.9152349 | 0.0002459 | 0.0022128 | 0.0022128 | 0.0022128 |
hemorrhage_simple = glm(hemorrhage_any ~ discordance_any + duration_to_lab + nearest_bilirubin + fibrinogen + international_normalization_ratio + weight + heparin_infusion_dose_value_units_kg_hr, data = model_fit_complications, family = binomial)
hemorrhage_simple %>% broom::tidy(exponentiate = TRUE, conf.int = TRUE)
## # A tibble: 8 × 7
## term estimate std.error statistic p.value conf.low conf.high
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 1.44 3.15 0.116 0.908 0.00507 1826.
## 2 discordance_anydiscor… 0.351 1.25 -0.836 0.403 0.0154 3.27
## 3 duration_to_lab 1.000 0.00402 -0.102 0.919 0.991 1.01
## 4 nearest_bilirubin 0.635 0.451 -1.01 0.314 0.204 1.21
## 5 fibrinogen 1.000 0.00191 -0.152 0.879 0.996 1.00
## 6 international_normali… 4.12 2.25 0.629 0.529 0.0138 173.
## 7 weight 0.938 0.0209 -3.07 0.00218 0.896 0.974
## 8 heparin_infusion_dose… 1.17 0.0900 1.74 0.0818 0.989 1.41
thrombosis_simple = glm(thrombosis_any ~ discordance_any + duration_to_lab + international_normalization_ratio + weight + heparin_infusion_dose_value_units_kg_hr + nearest_bilirubin + fibrinogen, data = model_fit_complications, family = binomial)
thrombosis_simple %>% broom::tidy(exponentiate = TRUE, conf.int = TRUE)
## # A tibble: 8 × 7
## term estimate std.error statistic p.value conf.low conf.high
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) 0.994 1.86 -0.00297 9.98e-1 0.0268 48.6
## 2 discordance_anydiscor… 2.96 0.513 2.12 3.42e-2 1.10 8.34
## 3 duration_to_lab 0.993 0.00349 -1.90 5.77e-2 0.986 1.000
## 4 international_normali… 0.591 1.23 -0.428 6.69e-1 0.0397 6.26
## 5 weight 1.03 0.0103 2.87 4.17e-3 1.01 1.05
## 6 heparin_infusion_dose… 0.787 0.0629 -3.82 1.36e-4 0.690 0.884
## 7 nearest_bilirubin 1.05 0.133 0.367 7.13e-1 0.791 1.34
## 8 fibrinogen 0.999 0.000927 -1.04 2.98e-1 0.997 1.00
complication_simple = glm(coag_event_any ~ unusual_type + duration_to_lab + nearest_bilirubin + fibrinogen + international_normalization_ratio + weight + heparin_infusion_dose_value_units_kg_hr, data = model_fit_complications, family = binomial)
complication_simple %>% broom::tidy()
## # A tibble: 8 × 5
## term estimate std.error statistic p.value
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 (Intercept) -2.66e+ 1 545666. -4.87e- 5 1.000
## 2 unusual_typeaPTT < antiXa -5.55e-16 124884. -4.44e-21 1
## 3 duration_to_lab 1.77e-17 625. 2.83e-20 1
## 4 nearest_bilirubin 1.22e-15 50094. 2.44e-20 1
## 5 fibrinogen 2.92e-18 298. 9.80e-21 1
## 6 international_normalization_ratio -1.93e-14 349714. -5.52e-20 1
## 7 weight -1.15e-16 2719. -4.23e-20 1
## 8 heparin_infusion_dose_value_units_kg_hr 1.08e-15 11855. 9.12e-20 1
Standard logistic regression had high separation based on discordance type, likely due to uncommon outcome among a relatively small sample size with several critical covariables. Thus, Firth penalized logistic regression used to compute odds ratios and p-values.
thrombotic and hemorrhagic events are common among ECMO patients
thrombotic events occurred frequently, despite heparin use in this sample
there is moderate correlation between aPTT and antiXa among heparinized ECMO patients
categorizing discordance based on aPTT range:anti-Xa range may not effectively recognize values which are relatively unusual (to each other) at CUIMC. The counts of patients who are discordant based on classic groupings based on usual CUIMC systemic anticoagulation goals are different (numerically higher) than those with relative comparison via polynomial model used in this approach. Our approach may better recognize truly unusual values.
initiation of ECMO induces a hypercoagulable state which is associated with thrombosis development
aPTT < antiXa is associated with increased risk of thrombosis compared to concordant and/or high aPTT > antiXa states (all patients with this state developed thrombosis in our study).
Likewise, aPTT < antiXa may be associated with a decreased risk of hemorrhage (non-significant)
underpowered for survival analysis
the relative aPTT:antiXa value is more important than absolute values. Classifying discordance by range alone (i.e. within wide goal ranges) may not appropriately recognize hyper- or hypocoagulable states.